
#include <set>
#include <vector>
#include <boost/shared_ptr.hpp>
#include <boost/algorithm/string.hpp>
#include <JSL.hpp>

#include "Animation.hpp"
#include "Mesh.hpp"
#include "Bone.hpp"
#include "../sys/Resources.hpp"
#include "../Material.hpp"
#include "../../debug.hpp"
#include "../../math.hpp"

#include "Definition.hpp"

namespace zen {

	namespace video {

		namespace model {

			Definition::Definition() {
				init();
			}

			Definition::Definition(const Definition &other) {
				*this = other;
			}

			Definition::~Definition() {
				free(false);
			}

			Definition& Definition::operator = (const Definition &other) {
				name = other.name;
				view = other.view;
				materialCount = other.materialCount;
				materials = new Material[materialCount];
				for (glm::uint i = 0; i < materialCount; i++)
					materials[i] = other.materials[i];
				boneCount = other.boneCount;
				bones = new Bone*[boneCount];
				for (glm::uint i = 0; i < boneCount; i++)
					bones[i] = new Bone(*other.bones[i]);
				meshCount = other.meshCount;
				meshes = new Mesh*[meshCount];
				for (glm::uint i = 0; i < meshCount; i++)
					meshes[i] = new Mesh(*other.meshes[i]);
				animationCount = other.animationCount;
				animations = new Animation*[animationCount];
				for (glm::uint i = 0; i < animationCount; i++)
					animations[i] = new Animation(*other.animations[i]);
				return *this;
			}

			void Definition::init() {
				materialCount = 0;
				materials = NULL;
				boneCount = 0;
				bones = NULL;
				meshCount = 0;
				meshes = NULL;
				animationCount = 0;
				animations = NULL;
				//modelManager = NULL;
			}

			void Definition::free(bool reinitialize) {
				delete[] materials;
				for (glm::uint i = 0; i < boneCount; i++)
					delete bones[i];
				delete[] bones;
				for (glm::uint i = 0; i < meshCount; i++)
					delete meshes[i];
				delete[] meshes;
				for (glm::uint i = 0; i < animationCount; i++)
					delete animations[i];
				delete[] animations;
				if (reinitialize)
					init();
			}

			Bone* Definition::getBone(const std::string &boneName) const {
				for (glm::uint i = 0; i < boneCount; i++)
					if (bones[i]->getName() == boneName)
						return bones[i];
				return nullptr;
			}

			Mesh* Definition::getMeshByBone(const std::string &boneName) const {
				Bone* bone = getBone(boneName);
				if (bone->getMeshIndex() >= 0)
					return meshes[bone->getMeshIndex()];
				return nullptr;
			};

			Mesh* Definition::getMeshByBone(glm::uint boneIndex) const {
				Bone* bone = getBone(boneIndex);
				if (bone->getMeshIndex() >= 0)
					return meshes[bone->getMeshIndex()];
				return nullptr;
			};

			bool Definition::createBoneHierarchy() {
				if (bones == nullptr)
					return true;
				model::Bone* tmp;
				int ppos;
				std::map<std::string, glm::uint> indices;
				std::map<std::string, glm::uint>::iterator iter;
				/// fill the map.
				for (glm::uint i = 0; i < boneCount; i++)
					indices[bones[i]->getName()] = i;
				/// sort pointers.
				for (glm::uint i = 0; i < boneCount; i++) {
					iter = indices.find(bones[i]->getParentName());
					if (iter != indices.end() && iter->second > i) {
						ppos = iter->second;
						indices[bones[i]->getName()] = ppos;
						indices[bones[i]->getParentName()] = i;
						tmp = bones[i];
						bones[i] = bones[ppos];
						bones[ppos] = tmp;
						i--;
					}
				}
				/// update indexes.
				for (glm::uint i = 0; i < boneCount; i++) {
					iter = indices.find(bones[i]->getParentName());
					bones[i]->setIndex(i);
					bones[i]->setParentIndex(iter != indices.end() ? iter->second : -1);
				}
				return true;
			}

			bool Definition::loadResource(const std::string &resource) {
				zen_call("Model.loadResource(\"" + resource + "\")");
				bool res = false;
				jsl::Node* src = sys::Resources::load(resource);
				if (src != NULL) {
					this->name = resource;
					res = loadResource(src);
					delete src;
				}
				zen_if_log(!res, "Error: Could not load resource \"" + resource + "\".");
				return res;
			}

			bool Definition::loadResource(jsl::Node *node) {
				zen_call("Model.loadResource(jsl::Node)");
				if (node == NULL) {
					zen_log("Error: Invalid resource, must not be null.");
					return false;
				}

				jsl::Node* tmp;
				jsl::Node *tmp2;

				free();
				view = glm::mat4();

				try {
					std::map<std::string, glm::uint> materialMap;
					tmp = node->at("materials");
					materialCount = (glm::uint)tmp->size();
					if (materialCount > 0) {
						materials = new Material[materialCount]();
						for (glm::uint i = 0; i < materialCount; i++) {
							tmp2 = tmp->at(i);
							materials[i].loadResource(tmp2, textureManager);
							materialMap[tmp2->at("name")->value()] = i;
						}
					}

					tmp = node->at("nodes");
					boneCount = (glm::uint)tmp->size();
					if (tmp->size() > 0) {
						std::vector<model::Mesh*> meshVector;
						std::vector<model::Animation*> animationVector;
						bones = new model::Bone*[boneCount];
						for (glm::uint i = 0; i < boneCount; i++) {
							bones[i] = new model::Bone();
							bones[i]->loadResource(tmp->at(i), meshVector, animationVector);
							std::map<std::string, glm::uint>::iterator it = materialMap.find(bones[i]->getMaterialName());
							bones[i]->setMaterialIndex(it == materialMap.end() ? -1 : it->second);
						}
						meshCount = (glm::uint)meshVector.size();
						if (meshCount > 0) {
							meshes = new model::Mesh*[meshCount];
							memcpy(meshes, &meshVector[0], sizeof(model::Mesh*) * meshCount);
						}
						animationCount = (glm::uint)animationVector.size();
						if (animationCount > 0) {
							animations = new model::Animation*[animationCount];
							memcpy(animations, &animationVector[0], sizeof(model::Animation*) * animationCount);
						}
					}

					if (!createBoneHierarchy()) {
						zen_log("Error: Couldn't create bone hierarchy.");
						return false;
					}
				} catch (const std::exception &e) {
					zen_log("Error: " + std::string(e.what()));
					return false;
				}
				return true;
			}


		} // namespace model

	} // namespace video

} // namespace zen